function loadAndValidateManifest {
    param ($bspPath) 

    $xsd = @"
<?xml version="1.0" encoding="utf-8"?>
<xs:schema targetNamespace="DevboardBspManifestSchema.xsd"
    elementFormDefault="qualified"
    xmlns="DevboardBspManifestSchema.xsd"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
>
  <xs:element name="FeatureManifest" type="FeatureManifestType"/>

  <xs:complexType name="FeatureManifestType">
    <xs:sequence>
      <xs:element name="BasePackages" type="BasePackagesType"/>
      <xs:element name="Features" type="FeaturesType"/>
    </xs:sequence>
  </xs:complexType>

  <xs:complexType name="BasePackagesType">
    <xs:sequence minOccurs="0" maxOccurs="unbounded">
      <xs:element name="PackageFile" type="PackageType"/>
    </xs:sequence>
  </xs:complexType>

  <xs:complexType name="PackageType">
    <xs:attribute name="Path">
      <xs:simpleType>
        <xs:restriction base="xs:string">
          <xs:pattern value="^%PKGDIR%\\.*"/>
        </xs:restriction>        
      </xs:simpleType>
    </xs:attribute>
    <xs:attribute name="Name" type="xs:string"/>
  </xs:complexType>
  
  <xs:complexType name="FeaturesType">
    <xs:sequence minOccurs="1" maxOccurs="1">
      <xs:element name="OEM">
        <xs:complexType>
          <xs:sequence minOccurs="0" maxOccurs="unbounded">
            <xs:choice>
              <xs:element name="FirmwarePackage" type="PackageType"/>
              <xs:element name="ToolPackage" type="ToolPackageType"/>
            </xs:choice>
          </xs:sequence>
        </xs:complexType>
      </xs:element>
    </xs:sequence>
  </xs:complexType>

  <xs:complexType name="ToolPackageType">
    <xs:complexContent>
      <xs:extension base="PackageType">
        <xs:attribute name="Type">
          <xs:simpleType>
            <xs:restriction base="xs:string">
              <xs:enumeration value="Windows"/>
              <xs:enumeration value="Uefi"/>
            </xs:restriction>
          </xs:simpleType>
        </xs:attribute>
      </xs:extension>
    </xs:complexContent>
  </xs:complexType>

</xs:schema>
"@

    $schemaReader = New-Object -TypeName System.IO.StringReader -ArgumentList @($xsd)
    $schema = [System.Xml.Schema.XmlSchema]::Read($schemaReader, $null)

    # Note: would prefer to use -Raw parameter for Get-Content, but it doesn't work well
    #       with pester's mocks.  Use -join instead to combine the strings back into a 
    #       single test block

    $text = (Get-Content -Path $bspPath) -join ""
    $textReader = New-Object -TypeName System.IO.StringReader -ArgumentList @($text)
     
    # validate the document against the schema
    $v = New-Object -TypeName System.Xml.XmlReaderSettings
    $v.ValidationType = [System.Xml.ValidationType]::Schema
         
    $v.Schemas.Add($schema) | Out-Null
    $xmlReader = [System.Xml.XmlReader]::Create($textReader, $v)
             
    $doc = New-Object -TypeName System.Xml.XmlDocument
     
    # load the document & validate the XML.
    $doc.Load($xmlReader)

    return $doc
}

function getNamespaceManager([xml] $doc) {
    $xmlNs = New-Object System.Xml.XmlNamespaceManager -ArgumentList @($doc.NameTable)
    $doc.DocumentElement.Attributes | 
        ForEach-Object {
            $name = $_.Name -replace "xmlns","" -replace ":",""
            $xmlNs.AddNamespace($name, $_.Value)
        }
    $xmlNs.AddNamespace("bsp", "DevboardBspManifestSchema.xsd") | Out-Null
    return ,$xmlNs
}

# loads the BSP index from the specified location.  Returns an array of 
# items, relative to the root, to be applied to the target image and the 
# actions to perform on those items.  
function loadBspIndex($bspFile) {

    if ((Test-Path -Path $bspFile) -eq $false) {
        throw "Cannot find BSP index $bspFile"
    }

    # Load and validate the XML file
    [xml] $bsp = loadAndValidateManifest $bspFile

    $xmlNs = getNamespaceManager $bsp

    # create items for the types of content we support
    
    $list = @($bsp.SelectNodes("//bsp:PackageFile | //bsp:FirmwarePackage | //bsp:ToolPackage", $xmlNs) |
        ForEach-Object {
            $elementName = $_.psbase.Name
            return [PSCustomObject] @{
                Name = $_.Name;
                Path = join-path $_.Path $_.Name;
                Type = switch($_) {
                    {$_.psbase.Name -eq "PackageFile"}     {"DriverPackage"}
                    {$_.psbase.Name -eq "FirmwarePackage"} {"Firmware"}
                    {$_.psbase.Name -eq "ToolPackage" -and
                     $_.Type -eq "Windows"}                {"WindowsTool"}
                    {$_.psbase.Name -eq "ToolPackage" -and 
                     $_.Type -eq "Uefi"}                   {"UefiTool"}
                    default {throw "Unrecognized element " + $_.OuterXml}
                }
            }
        })

    return $list,(Split-Path (Resolve-Path -Path $bspFile) -Parent)
}

function createMountDir($mountDir) {
    if ($mountDir -eq $null) {
        $name = ("scmount-" + (get-date -UFormat "%y%m%d-%H%M%S-%U"))
        $mountDir = join-Path $env:TEMP $name
        New-Item -Path $mountDir -ItemType Directory > $null
        $tempDir = $true
    } elseif ((Test-Path -Path $mountDir -PathType Container) -eq $false) {
        New-Item -Path $mountDir -ItemType Directory > $null
        $tempDir = $false
    } else {
        $tempDir = $false
    }

    Write-Debug "MountDirectory=$MountDir"
    return $mountDir,$tempDir
}

function getLogPath($destination) {
    # DISM log files go into the same destination as the target WIM
    # Or local directory if we're updating an existing WIM
    if ($Destination -eq $null) {

        $logPath = ".\dism.log"

    } else {

        if ((split-path $Destination -Parent) -eq "") {
            $Destination = join-path "." $Destination
        }

        $logPath = join-path (split-path $Destination -Parent) "dism.log"
    }

    Write-Debug "Log path = $logPath"

    return $logPath
}

function getWdkRoot($customWdkRoot) {

    $wdkRegKey = "HKLM:\Software\Microsoft\Windows Kits\Installed Roots"

    if ($customWdkRoot -ne $null) {
        return $customWdkRoot 
    }

    if ($ENV:WDKContentRoot -ne $null) {
        return $ENV:WDKContentRoot
    }

    if (Test-Path -Path $wdkRegKey) {
        $key = Get-Item -Path $wdkRegKey

        # get the highest valued versioned KitsRoot entry
        # that will be at least Windows 8.1 kit.
        $propName = $key.Property | Where-Object {$_ -match "KitsRoot\d+"} | Sort-Object -Descending | Select-Object -First 1

        if ($propName -ne $null) {
            return ($key | Get-ItemProperty).$propName
        }
    }

    return $null
}

function copyItemToWim {
    param (
        [parameter(Mandatory=$true)] 
        [ValidateSet("CopyItem", "CopyFile", "CopyDirectory", "CopyContents")] 
        $CopyType,
        [parameter(Mandatory=$true)] $SourcePath,
        [parameter(Mandatory=$true)] $ImageRoot,
        [parameter(Mandatory=$true)] $ImageDirectory,
        $NewName
        )

    $item = Get-Item $SourcePath

    $src = Get-Item -Path $SourcePath
    if ($src -is [array]) {throw "copyItemToWim: $SourcePath must resolve to a single item"}

    switch ($CopyType) {
        "CopyFile" {
            if ($src.PSIsContainer -eq $true) {throw "copyItemToWim: $CopyType cannot accept directory ($SourcePath)"}
            $recurse = $false
            break;
        }

        {$_ -in ("CopyDirectory","CopyContents")} {
            if ($src.PSIsContainer -eq $false) {throw "CopyItemToWim: $CopyType requires source to be a container ($SourcePath)"}
            $recurse = $true
        }

        default {
            $recurse = $true 
            break;
        }
    }

    if ($PSBoundParameters.ContainsKey("NewName")) {
        $destinationName = $NewName
    } else {
        $destinationName = $src.Name
    }

    $destination = Join-Path $ImageRoot $ImageDirectory

    # Create the destination directory if it doesn't exist.  This simplifies
    # CopyDirectory vs. CopyContents
     if ((Test-Path -PathType Container -Path $destination) -eq $false) {
        New-Item -Path $destination -ItemType Directory | Out-Null
    }

    $destination = Join-Path $destination $(if ($NewName -ne $null) {$NewName} else {$item.Name})

    Copy-Item -Path $src.FullName -Destination $destination -Recurse:$recurse -Force | Out-Null
}

function createShortcut {
	param($FileName, $Destination)

    $wsh = New-Object -ComObject "WScript.Shell"

    $shortcut = $wsh.CreateShortcut($FileName)

    $shortcut.TargetPath = $Destination
    $shortcut.Save()
}

function New-DevboardImage {
<#

.SYNOPSIS

Applies a board support package (BSP) to a Windows image File (a .WIM file) to create a Windows image provisioned for the specified board.

.DESCRIPTION

New-DevboardImage will mount the specified Windows image file (WIM) and apply the contents of the specified board support package (BSP).  The drivers specified in the BSP will be installed, and any tools or firmware packages copied to the tools directory in the root of the image.

Additionally if the Windows 8.1 (or higher) WDK is installed on the system, the script will copy the tools from the kits into the tools directory in the image.

.PARAMETER SourcePath

The path to the Windows Image File to apply the board support package to.

.PARAMETER Index

If SourcePath points to a multi-image WIM, Index specifies the image within the WIM to modify.

Optional for a single-image WIM.

.PARAMETER BspManifest

The path to the manifest file for the BSP.  Generally this is found in the root of the BSP.

.PARAMETER Destination

The name of the Windows Image File that the script should create.  If not specified then SourcePath is modified in place.

.PARAMETER ToolsPath

The path to a directory of content that the caller wants copied into the Windows image.  The contents will be copied into \tools\other in the target image.

.PARAMETER WdkPath

The path to the root of the Windows Driver Kit.  If not specified then the script will use the WDKCONTENTROOT environment variable or the kits root specified in the registry to locate the WDK.

.PARAMETER AnswerFile

If specified the script will apply an AnswerFile to the Windows image while it is mounted.

.PARAMETER MountDir

Specifies the directory where the script should temporarily mount the Windows image while modifying it.  If not specified, a directory under the user's temporary directory is used.

.EXAMPLE

Apply the BSP to the second image in the WIM file, and save the resulting image in the local directory:

  New-DevboardImage -SourcePath d:\sources\install.wim -Index 2 -BspManifest .\BSP\BspManifest.xml -Destination .\modified.wim

.NOTES

This script must be run as administrator within an elevated powershell session.

#>
    param (
        [parameter(Mandatory=$true)]
        $SourcePath,

        $Index,

        [parameter(Mandatory=$true)]
        $BspManifest,

        $Destination,

        $ToolsPath,

        $WdkPath,

        $AnswerFile,

        $MountDir
        )

    $ErrorActionPreference = "Stop"

    # Initial assumptions:
    #    * Assume script failure.  Drives cleanup afterwards
    #    * Assume we shouldn't clean up the mount directory

    $success = $false

    # Create a temporary directory to mount the WIM if the user doesn't provide one
    $MountDir,$cleanupMountDir = createMountDir $MountDir

    $logPath = getLogPath $Destination 

    # Import the BSP
    $bspContent,$bspRoot = loadBspIndex $BspManifest

    # locate the WDK directory.
    $wdkRoot = getWdkRoot $WdkPath

    # setup some default paths
    $defaultProfile = "\Users\Default\Desktop"
    $toolsDir = "tools"

    # Check for a multi-image index, and throw an error if the image to use 
    # is ambiguous
    $images = @(Get-WindowsImage -ImagePath $SourcePath)

    if (($images.Count -gt 1) -and ($Index -eq $null)) {
        throw "Must specify -Index when -SourcePath is a multi-image WIM"
    } elseif ($Index -eq $null) {
        $Index = $images[0].Index
    }

    # depending on mode
    if ($Destination -ne $null) {
        # Copy the specified image from the source WIM to the destination WIM
        if (Test-Path $Destination) {
            Remove-Item $Destination > $null
        }
        Write-Progress -Activity "Creating Developer Board Image" -CurrentOperation "Copying Image"
        Export-WindowsImage -SourceImagePath $SourcePath -SourceIndex $Index -DestinationImagePath $Destination -LogPath $logPath | Out-Null 

        # Mount the destination WIM
        Write-Progress -Activity "Creating Developer Board Image" -CurrentOperation "Mounting Image"
        Mount-WindowsImage -ImagePath $Destination -Index 1 -Path $mountDir -LogPath $logPath | Out-Null
    } else {

        # Mount the source WIM to modify it locally
        Write-Progress -Activity "Creating Developer Board Image" -CurrentOperation "Mounting Image"
        Mount-WindowsImage -ImagePath $SourcePath -Index $Index -Path $mountDir -LogPath $logPath | Out-Null 
    }

    try {

        # copy scripts to the default profile desktop
        copyItemToWim -CopyType CopyItem `
                      -SourcePath "$PSScriptRoot\enable-telnet.ps1" `
                      -ImageRoot $mountDir `
                      -ImageDirectory $defaultProfile

        $winDir = join-path $MountDir "windows"

        # Apply an unattend-file if the user supplied one
        if ($AnswerFile -ne $null) {
            Write-Progress -Activity "Creating Developer Board Image" -CurrentOperation "Applying Unattend File"
            Apply-WindowsUnattend -Path $MountDir -UnattendPath $AnswerFile  -LogPath $logPath | Out-Null
        }

        # Add BSP content
        Write-Progress -Activity "Applying Developer Board Image"
        for ($i = 0; $i -lt $bspContent.Count; $i += 1) {
            $item = $bspContent[$i]
            $itemPath = $item.Path -replace "%PKGDIR%",$BspRoot

            Write-Verbose "Adding $($item.Type) $($item.Name) from $itemPath"
            Write-Progress -Activity "Creating Developer Board Image" -CurrentOperation "Adding $($item.Type) $($item.Name) to image" -PercentComplete ($i * 100 / $bspContent.Count)

            switch ($item.Type) {
                DriverPackage {
                    Add-WindowsDriver -Path $MountDir -Driver $itemPath -ForceUnsigned -LogPath $logPath | Out-Null
                    break
                }

                Firmware {
                    copyItemToWim -CopyType CopyFile -SourcePath $itemPath -ImageRoot $MountDir -ImageDirectory "$toolsDir\Firmware"
                    break
                }

                UefiTool {
                    copyItemToWim -CopyType CopyItem -SourcePath $itemPath -ImageRoot $MountDir -ImageDirectory "$toolsDir\UEFI"
                    break
                }

                WindowsTool {
                    copyItemToWim -CopyType CopyItem -SourcePath $itemPath -ImageRoot $MountDir -ImageDirectory "$toolsDir\Windows"
                    break
                }

                default { throw "Unsupported content type $($item.Type)" }
            }

            Write-Progress -Activity "Creating Developer Board Image" -CurrentOperation "Adding $($item.Type) $($item.Name) to image" -Completed
        }

        # Copy tools to the image
        if ($ToolsPath -ne $null) {
            # copy tools to the boot disk 
            Write-Progress -Activity "Creating Developer Board Image" -CurrentOperation "Copying tools from $ToolsPath"
            copyItemToWim -CopyType CopyItem -SourcePath $ToolsPath -ImageRoot $MountDir -ImageDirectory "$toolsDir\other"
        }

        # Copy WDK content to the image
        if ($wdkRoot -ne $null -and (Test-Path $wdkRoot\tools)) {
            Write-Progress -Activity "Creating Developer Board Image" -CurrentOperation "Copying WDK tools"

            # copy WDK tools to the boot disk 
            copyItemToWim -CopyType CopyContents -SourcePath (join-path $wdkRoot tools) -ImageRoot $MountDir -ImageDirectory "$toolsDir" -NewName "WDK"
        
            # and the debugger MSI so the developer can install the debugger
            copyItemToWim -CopyType CopyFile -SourcePath (join-Path $wdkRoot "Debuggers\Redist\X86 Debuggers And Tools-x86_en-us.msi") -ImageRoot $MountDir -ImageDirectory "$toolsDir\debugger"
        } else {
            Write-Warning "Cannot find installed WDK, some development tools may be missing from the OS image"
        }

        # Add a shortcut to the tools directory into the default profile
        createShortcut $MountDir\Users\Default\Desktop\tools.lnk c:\tools

        $success = $true

    } finally {

        Write-Progress -Activity "Creating Developer Board Image" -CurrentOperation "Dismounting Image"
        if ($success) {
            Dismount-WindowsImage -Path $MountDir -Save -LogPath $logPath | Out-Null
        } else { 
            Dismount-WindowsImage -Path $MountDir -Discard -LogPath $logPath | Out-Null 

            if ($Destination -ne $null) {
                Remove-Item $Destination
            }
        }

        if ($cleanupMountDir) {
            Remove-Item -Recurse -Force $MountDir 
        }

        Write-Progress -Activity "Creating Developer Board Image" -Completed
    }
}